¿Cuál es el problema?
El aislamiento de instantánea (MVCC) gestiona bien los conflictos lectura-escritura. Pero tiene un punto ciego: los conflictos escritura-escritura.
✓ MVCC resuelve solo
Lectura-escrituraT lee un dato que T' está modificando → MVCC devuelve la versión correcta del snapshot. Sin esperas, sin error.
✗ MVCC necesita ayuda
Escritura-escrituraT y T' modifican el mismo dato concurrentemente → MVCC por sí solo no puede garantizar la serializabilidad. Se necesita una estrategia adicional.
Dos estrategias para resolver conflictos escritura-escritura
Al hacer
COMMIT, T comprueba si T' ya commitó sobre el mismo dato. Si sí → rollback implícito de T.
T mantiene un bloqueo de escritura hasta el fin. El conflicto se resuelve en favor de quien bloqueó primero.
Oracle y PostgreSQL usan Primera actualización gana. SQL-Server con snapshot isolation usa Primer commit gana.
Ejemplo motivador — Reserva de asientos
Dos operadores intentan reservar el mismo asiento 10 del avión X para clientes distintos al mismo tiempo. Sin control → actualización perdida: ambos creen haber reservado con éxito pero un cliente se queda sin asiento. Con Primera actualización gana, el segundo recibe un error y puede reintentar.
Caso 1 — Dato sin bloquear
T quiere escribir un dato que no está bloqueado por ninguna otra transacción activa. ¿Puede escribir libremente?
commit sobre ese dato. Si ts_fin(T') > ts_inicio(T) → T fue "adelantada" → rollback implícito de T.
Caso 2 — Dato bloqueado
T quiere escribir un dato que ya está bloqueado por una transacción activa T'. T debe esperar. Lo que pase después depende de cómo acabe T'.
T' libera el bloqueo. T entra al Caso 1 y continúa normalmente.
T' libera el bloqueo. T aborta con rollback implícito y lanza un error (
ORA-08177 / SQL:40001).
Subapartados 6.2.3.a – d
6.2.3.a — Conflictos de escritura con distintos niveles de aislamiento
Cuando una transacción SERIALIZABLE detecta un conflicto, actúa como si todas las concurrentes también fueran serializables, independientemente de su nivel real de aislamiento.
→ T1 no recibe ningún error (fue la primera en confirmar).
→ Detecta conflicto →
ORA-08177 / SQL:40001.
6.2.3.b — Conflicto al borrar la misma fila
Dos transacciones leen una fila y, en base a su valor, ambas deciden borrarla. Esto genera un ciclo en el grafo de precedencia:
T2 → T1 (T2 debe leer antes de que T1 borre)
→ Ciclo → NO serializable
La segunda transacción espera, y si la primera hizo COMMIT, recibe el error de serialización.
6.2.3.c — Conflicto entre campos distintos de la misma fila
campo1 y T2 modifique campo2 de la misma fila, los SGBDs usan granularidad de fila. El conflicto se detecta igualmente → mismo error.
Esto previene la actualización perdida en aplicaciones que hacen UPDATE volcando todo el formulario. Si dos usuarios editan datos distintos del mismo registro y uno no ve los cambios del otro → el segundo sobrescribe sin querer el cambio del primero.
-- T1 cambió teléfono, T2 cambia email pero vuelca el teléfono viejo
6.2.3.d — Actualizaciones perdidas con READ COMMITTED
Cuando no se puede usar SERIALIZABLE, hay dos alternativas para evitar la actualización perdida:
SELECT ... FOR UPDATE bloquea la fila antes de que el usuario la edite. Nadie más puede modificarla mientras el usuario trabaja. Garantía total pero puede generar esperas largas.
No se bloquea al leer. En el momento del UPDATE se comprueba un campo
version o ts. Si la fila cambió desde que se leyó → se informa al usuario y se reintenta. Menos esperas, más reintentos.
6.2.3.b — Conflicto al borrar la misma fila
Dos transacciones leen una fila, comprueban su valor y ambas deciden borrarla en función de ese valor. El mecanismo de detección del conflicto es idéntico al Caso 2, pero el razonamiento sobre por qué es un problema es más sutil.
Estado inicial de la tabla
id=1 | campo1 = NULL
Lógica de cada transacción: "Si campo1 es NULL, borrar la fila." Como campo1 es NULL, ambas transacciones decidirán borrar.
¿Por qué no es serializable?
Si se ejecutaran en serie, la segunda transacción no encontraría la fila (ya la borró la primera) y no podría ni leer el valor para decidir. El resultado sería diferente al de la ejecución concurrente → la planificación concurrente no es equivalente a ninguna serie.
Bloqueo Pesimista vs. Bloqueo Optimista
Cuando usamos READ COMMITTED (el nivel por defecto en la mayoría de SGBDs) el SGBD no detecta automáticamente el problema de la actualización perdida. Hay que resolverlo a mano. Existen dos estrategias opuestas.
🔒 Bloqueo Pesimista
Filosofía: "Asumo que habrá conflicto, así que bloqueo antes de que ocurra."Se usa
SELECT ... FOR UPDATE para bloquear la fila en el momento de leerla, antes de que el usuario empiece a editar. Nadie más puede modificar la fila hasta que la transacción termine.Ventaja: Garantía total. Imposible que haya actualización perdida.
Inconveniente: Genera esperas. Si el usuario tarda en guardar, otros quedan bloqueados.
🎯 Bloqueo Optimista
Filosofía: "Asumo que no habrá conflicto, leo sin bloquear y compruebo al guardar."Se lee sin bloqueo. Se guarda un campo
version (o timestamp). En el UPDATE se añade una condición: WHERE id=1 AND version=X. Si la fila cambió desde que se leyó, el UPDATE afecta 0 filas → se detecta el conflicto y se informa al usuario.Ventaja: Sin esperas. Alta concurrencia.
Inconveniente: El usuario puede perder su trabajo si hubo conflicto y debe reintentar.
FOR UPDATE al abrir el formulario, bloqueando la fila. El segundo usuario queda en espera hasta que el primero guarde.
¿Cuándo usar cada estrategia?
La probabilidad de conflicto es alta (muchos usuarios editan los mismos registros), o cuando el coste de reintentar es grande (operaciones largas, datos críticos).
Los conflictos son raros (usuarios distintos suelen editar registros distintos), o cuando el tiempo de edición puede ser largo y no quieres mantener bloqueos abiertos.